type ChartName = 'trend' | 'map' | 'seller' | 'hot' | 'rank' | 'stock'

type Listener<T = unknown> = (payload: T) => void

type PayloadEnvelope = {
  action?: string
  chartName?: string
  data?: unknown
}

const listeners = new Map<ChartName, Set<Listener>>()
const pendingCharts = new Set<ChartName>()

let ws: WebSocket | null = null
let reconnectTimer: number | null = null

const WS_URL = (import.meta.env?.VITE_WS_URL as string | undefined) || 'ws://localhost:9998'
const RECONNECT_DELAY = 3000

function scheduleReconnect() {
  if (reconnectTimer !== null) {
    return
  }
  reconnectTimer = window.setTimeout(() => {
    reconnectTimer = null
    ensureSocket()
  }, RECONNECT_DELAY)
}

function notifyListeners(chartName: ChartName, rawData: unknown) {
  const group = listeners.get(chartName)
  if (!group || group.size === 0) {
    return
  }

  let payloadData: unknown = rawData

  if (typeof rawData === 'string') {
    try {
      payloadData = JSON.parse(rawData)
    } catch (error) {
      console.warn(`[dataHub] JSON.parse failed for chart ${chartName}`, error)
    }
  }

  group.forEach(listener => {
    try {
      listener(payloadData)
    } catch (error) {
      console.error('[dataHub] listener execution failed', error)
    }
  })
}

function handleMessage(event: MessageEvent) {
  const { data } = event
  let payload: PayloadEnvelope

  try {
    payload = typeof data === 'string' ? JSON.parse(data) : data
  } catch (error) {
    console.warn('[dataHub] invalid message received', data, error)
    return
  }

  if (payload?.action !== 'getData') {
    return
  }

  const chartName = payload.chartName as ChartName | undefined
  if (!chartName || !listeners.has(chartName)) {
    return
  }

  notifyListeners(chartName, payload.data)
}

function sendGetData(chartName: ChartName) {
  pendingCharts.add(chartName)
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    ensureSocket()
    return
  }

  const message = JSON.stringify({
    action: 'getData',
    chartName,
  })

  try {
    ws.send(message)
  } catch (error) {
    console.error('[dataHub] failed to send message', error)
  }
}

function wireSocket(socket: WebSocket) {
  socket.onopen = () => {
    pendingCharts.forEach(chartName => {
      const message = JSON.stringify({ action: 'getData', chartName })
      try {
        socket.send(message)
      } catch (error) {
        console.error('[dataHub] failed to request data after reconnect', error)
      }
    })
  }

  socket.onmessage = handleMessage

  socket.onerror = error => {
    console.error('[dataHub] websocket error', error)
  }

  socket.onclose = () => {
    ws = null
    scheduleReconnect()
  }
}

function ensureSocket() {
  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
    return
  }

  try {
    ws = new WebSocket(WS_URL)
    wireSocket(ws)
  } catch (error) {
    console.error('[dataHub] failed to open websocket connection', error)
    scheduleReconnect()
  }
}

export function subscribeToChart<T = unknown>(chartName: ChartName, listener: Listener<T>) {
  let group = listeners.get(chartName)
  if (!group) {
    group = new Set()
    listeners.set(chartName, group)
  }

  group.add(listener as Listener)

  sendGetData(chartName)

  return () => {
    const existingGroup = listeners.get(chartName)
    if (!existingGroup) {
      return
    }
    existingGroup.delete(listener as Listener)
    if (existingGroup.size === 0) {
      listeners.delete(chartName)
      pendingCharts.delete(chartName)
    }
  }
}

export function closeDataHub() {
  listeners.clear()
  pendingCharts.clear()
  if (reconnectTimer !== null) {
    window.clearTimeout(reconnectTimer)
    reconnectTimer = null
  }
  if (ws) {
    ws.close()
    ws = null
  }
}
